--[========================================================================[--

Game logic for Thrust II Reloaded.

Copyright © 2015 Pedro Gimeno Fortea

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

--]========================================================================]--

game = {}

local json = require "3rdparty/dkjson"

local garbagetimer = 0
local collision_canvas, no_collision
local enemies, ship, orb, orbs, counters
local tileset, tilecoll, spriteset, spritecoll
local tilequads, spritequads
local orb_sprite
local lastxtile, lastytile
local fineangle
local was_turning, shooting

-- cached namespaces
local map, enemytypes, decoys, targets, respawn, agents, keys

local tractor = false -- may contain index of orb tracted

local tgt_tile_start = 122
local tgt_frames = 2 -- how many frames in animation
local tgt_tile_current = tgt_tile_start
local tgt_fps = 8/25
local tgt_timer = 0
local tgt_index -- index of current target in the targets table
-- squared velocity threshold for being able to drop an orb into the target
-- (can't drop if the orb crosses the target too fast)
local tgt_vel_threshold2 = 10000 -- 100 px/s

local tractor_length = 80
local tractor_length2 = tractor_length * tractor_length
local shoot_radius = 224


function game.load()
  -- Game viewport
  game.vx, game.vy, game.vw, game.vh = 0, 0, main.ww, main.wh

  -- Cache some namespaces
  map, enemytypes, decoys, targets, respawn, agents, keys = main.map, main.enemytypes,
    main.decoys, main.targets, main.respawn, main.agents, main.keys
  tileset = lg.newImage("img/Tiles32x32.png")
  tilecoll = lg.newImage("img/Tiles32x32_collision.png")
  spriteset = lg.newImage("img/Sprites32x32.png")
  spritecoll = lg.newImage("img/Sprites32x32_collision.png")
  local tx, ty = tileset:getDimensions()
  local sx, sy = spriteset:getDimensions()

  -- Tiles are easy
  tilequads = {}  -- used for both normal tiles and collision tiles
                  -- which means the images must have the same size
                  -- (same applies to spritequads below)
  for y = 0, 7 do
    for x = 0, 15 do
      tilequads[#tilequads + 1] = lg.newQuad(x*32, y*32, 32, 32, tx, ty)
    end
  end

  -- Sprites
  spritequads = {}

  -- Ship quads
  for y = 0, 1 do
    for x = 0, 15 do
      spritequads[#spritequads + 1] = lg.newQuad(x*32, y*32, 32, 32, sx, sy)
    end
  end

  -- Enemies quads
  -- Assumes that if there is a higher sprite in a row, then the space
  -- in the next rows until completing that height is empty.
  -- E.g. if X is 32x32 (1x1 cells) and Y is 64x64 (2x2 cells):
  --       X YY YY X X    correct
  --         YY YY    
  --
  --       X YY YY X X    incorrect - don't reuse the empty spaces
  --       X YY YY        (our algorithm isn't that clever)

  local idx = 32 -- 32x32 cell number - we start after the ship sprites
  local height = 1 -- height of the highest cell in the row
  for k, v in ipairs(enemytypes) do
    -- Cache the starting quad. The animation spans nframes from here.
    if v.nframes >= 1 then v.initsprite = #spritequads + 1 end
    for enemynum = 1, v.nframes do
      spritequads[#spritequads + 1] =
         lg.newQuad(idx%16*32, (idx-idx%16)/16*32, v.width*32, v.height*32, sx, sy)
      if v.height > height then height = v.height end
      idx = idx + v.width
      if idx % 16 == 0 then
        idx = idx + 32 * (height-1) -- skip row height
        height = 1
      end
    end
  end

  -- Finally, the orb
  orb_sprite = #spritequads + 1
  spritequads[orb_sprite] =
    lg.newQuad(idx%16*32, (idx-idx%16)/16*32, 32, 32, sx, sy)

  tilebatch = lg.newSpriteBatch(tileset,
                -- In 1D, a 33-pixel window can see up to two 32-pixel tiles
                -- simultaneously. One needs a 34-pixel window to be able
                -- to see three. So in 1D it would be: floor((widht+62)/32)
                -- which equals 2 for width=33 and 3 for width=34. In 2D the
                -- natural extension is the following.
                math.floor((game.vw + 62)*(game.vh + 62)/(32*32))
                -- For 640x480, that's 336 tiles. Less than the default 1000,
                -- so quite bearable.
              )

  collision_canvas = lg.newCanvas(32, 64) -- enough for the ship and orb sprite
  no_collision = string.rep("\0", #collision_canvas:getImageData():getString())

  -- How many orbs we're going to draw max. It's pretty static, it only changes
  -- when an orb is picked up.
  orbbatch = lg.newSpriteBatch(spriteset, #main.orbs + #main.decoys, "static")

end

function game.savegame()
  game.state.orb = orb -- it can be set to nil
  local f = json.encode(game.state, {indent=true})
  lfs.write("saved.txt", f, #f)
end

local function new_or_load_game(load)
  -- restore saved game or start new game

  if load and lfs.isFile("saved.txt") then
    local f, s = lfs.read("saved.txt")
    local tmp, err
    game.state, tmp, err = json.decode(f, 1, json.null, nil)
    if err then
      game.state = false
    else
      -- validate
      if #game.state.orbs > #main.orbs
        or #game.state.enemies > #main.enemies
        or game.state.counters.shields > 99
      then game.state = false end
    end

  else
    game.state = false
  end

  if not game.state then
    -- New game - copy initial state
    game.state = { enemies = main.deepcopy(main.enemies),
                   ship = { angle = 0 },
                   orbs = main.deepcopy(main.orbs),
                   counters = { shields = 10, score = 0, respawn = 1, clock = 0 },
                 }
    game.state.ship.x = main.respawn[game.state.counters.respawn].x
    game.state.ship.y = main.respawn[game.state.counters.respawn].y
    game.state.ship.oldx = game.state.ship.x -- initialize integrator with vel 0
    game.state.ship.oldy = game.state.ship.y

    -- Can't find a good place for this at the moment.
    game.state.counters.shields = game.state.counters.shields - 1
  end

  -- shortcuts
  enemies, ship, orb, orbs, counters =
    game.state.enemies, game.state.ship, game.state.orb, game.state.orbs,
    game.state.counters

  -- Clean up to start again
  orbbatch:clear()

  -- Add decoys
  for k, v in ipairs(decoys) do
    orbbatch:add(spritequads[orb_sprite], v.x-16, v.y-16)
  end

  -- Add normal orbs
  for k, v in ipairs(orbs) do
    v.sprite = orbbatch:add(spritequads[orb_sprite], v.x-16, v.y-16)
  end

  -- Update map with current progress
  tgt_index = #orbs + (orb and 1 or 0)
  for k, v in ipairs(targets) do
    if k <= tgt_index then
      map[v.y*128+v.x+1] = v.empty
    else
      map[v.y*128+v.x+1] = v.tile
    end
  end

  -- force refresh
  lastxtile = false

  -- assume no key pressed
  was_turning = false
  shooting = false

  -- initialize first frame
  game.update(0)
end


local function update_ship(dt)
  if dt == 0 then return end

  --[[ update angle ]]

  local angvel = 16

  local kleft, kright = lk.isDown(keys.left), lk.isDown(keys.right)
  if kleft and not kright then
    if was_turning then
      fineangle = (fineangle - angvel*dt) % 32
      ship.angle = math.ceil(fineangle) % 32
    else
      was_turning = true -- sharp reaction upon pressing
      fineangle = (ship.angle - 1) % 32
      ship.angle = fineangle
    end
  elseif kright and not kleft then
    if was_turning then
      fineangle = (fineangle + angvel*dt) % 32
      ship.angle = math.floor(fineangle)
    else
      was_turning = true
      fineangle = (ship.angle + 1) % 32
      ship.angle = fineangle
    end
  else
    was_turning = false
    fineangle = ship.angle
  end

  --[[ update ship (Verlet integrator) ]]

  -- flight parameters
  local thrust = 380
  local gravity = 45
  local dragx = 0.997
  local dragy = 0.998
  local shipmass = 1

  local dt2 = dt*dt

  local shipforcex, shipforcey = 0, gravity * shipmass

  thrusting = lk.isDown(keys.thrust)
  if thrusting then
    shipforcex = shipforcex + math.sin(fineangle*math.pi/16) * thrust
    shipforcey = shipforcey - math.cos(fineangle*math.pi/16) * thrust
  end

  local orbforcex = 0
  local orbforcey = 0
  local orbaccelx = 0
  local orbaccely = 0
  local neworbx
  local neworby
  local newshipx = ship.x
  local newshipy = ship.y
  local orbmass

  if orb then
    orbmass = orb.m * shipmass -- orb mass is how heavier than the ship
    neworbx = orb.x
    neworby = orb.y
    orbforcex = 0
    orbforcey = gravity * orbmass

    orbaccelx = orbforcex / orbmass
    orbaccely = orbforcey / orbmass

    neworbx = neworbx + (orb.x - orb.oldx) * dragx + orbaccelx * dt2
    neworby = neworby + (orb.y - orb.oldy) * dragy + orbaccely * dt2
  end

  local accelx = shipforcex / shipmass
  local accely = shipforcey / shipmass

  newshipx = newshipx + (newshipx - ship.oldx) * dragx + accelx * dt2
  newshipy = newshipy + (newshipy - ship.oldy) * dragy + accely * dt2

  if orb then
    -- Apply constraint
    local ropex = newshipx - neworbx
    local ropey = newshipy - neworby
    local actuallen = math.sqrt(ropex*ropex + ropey*ropey)
    if actuallen < 0.00001 then actuallen = 0.00001 end -- should never happen
    -- the distance to adjust for is tractor_length - actuallen
    -- ours is a rope - remove this condition if original behavior wanted
    if actuallen > tractor_length then
      -- Force of the ship over the orb
      local k = 30
      if #argv > 1 then k = tonumber(argv[2]) or 0 end
      shipforcex = (tractor_length - actuallen) * ropex / actuallen
                * k
      shipforcey = (tractor_length - actuallen) * ropey / actuallen
                * k
      orbforcex = -(tractor_length - actuallen) * ropex / actuallen
                * k
      orbforcey = -(tractor_length - actuallen) * ropey / actuallen
                * k

      accelx = shipforcex / shipmass
      accely = shipforcey / shipmass

      newshipx = newshipx + accelx * dt2
      newshipy = newshipy + accely * dt2

      orbaccelx = orbforcex / orbmass
      orbaccely = orbforcey / orbmass

      neworbx = neworbx + orbaccelx * dt2
      neworby = neworby + orbaccely * dt2
    end

    -- "Scroll" orb position
    orb.oldx = orb.x
    orb.oldy = orb.y
    orb.x = neworbx
    orb.y = neworby
  end

  -- "Scroll" ship position
  ship.oldx = ship.x
  ship.oldy = ship.y
  ship.x = newshipx
  ship.y = newshipy

  -- Normalize if reasonable
  if not orb or math.floor(ship.x / 4096) == math.floor(orb.x / 4096) then
    local k = math.floor(ship.x / 4096) * 4096
    ship.x = ship.x - k
    ship.oldx = ship.oldx - k
    if orb then orb.x = orb.x - k orb.oldx = orb.oldx - k end
  end
end

function game.newgame()
  new_or_load_game(main.restore)
end

local function collided()
  -- Collision test - paint the collision tiles/sprites to a canvas
  -- and multiply by ship's collision sprite to see if there are
  -- common pixels (all zeros = no)
  lg.setCanvas(collision_canvas)

  -- Clear collision canvas
  collision_canvas:clear()

  -- In order to only do getImageData only once, our canvas is
  -- divided into two halves vertically. The top part is for
  -- the ship, the bottom part is for the orb.
  for i = 0, (orb and 32 or 0), 32 do -- Draw twice if an orb is carried, else once.

    lg.setScissor(0, i, 32, 32) -- select which half to be drawn
    local topleftx = i == 0 and math.floor(ship.x-15.5)%4096 or math.floor(orb.x-16)%4096
    local toplefty = i == 0 and math.floor(ship.y-15.5)      or math.floor(orb.y-16)

    -- Colision with tile
    local localx = -(topleftx%32)
    local localy = -(toplefty%32)
    local tilex = (topleftx+localx)/32
    local tiley = (toplefty+localy)/32
    if tiley > 62 then tiley = 62 end
    if tiley < 0 then tiley = 0 end

    lg.draw(tilecoll, tilequads[map[tilex%128 + tiley*128 + 1]],  localx, localy+i)
    if localx + 32 < 32 then
      lg.draw(tilecoll, tilequads[map[(tilex+1)%128 + tiley*128 + 1]], localx+32, localy+i)
    end
    if localy + 32 < 32 then
      lg.draw(tilecoll, tilequads[map[tilex%128 + (tiley+1)*128 + 1]], localx, localy+32+i)
    end
    if localx + 32 < 32 and localy + 32 < 32 then
      lg.draw(tilecoll, tilequads[map[(tilex+1)%128 + (tiley+1)*128 + 1]], localx+32, localy+32+i)
    end

    -- Collision with decoys
    local vx, vy
    for k, v in ipairs(decoys) do
      vx = v.x-16 - topleftx - (topleftx < 32 and v.x >= 4064 and 4096 or 0)
      vy = v.y-16 - toplefty
      if vx > -32 and vx < 32 and vy > -32 and vy < 32 then -- only draw if in range
        lg.draw(spritecoll, spritequads[orb_sprite], vx, vy + i)
      end
    end

    -- Same for orbs
    for k, v in ipairs(orbs) do
      vx = v.x-16 - topleftx - (topleftx < 32 and v.x >= 4064 and 4096 or 0)
      vy = v.y-16 - toplefty
      if vx > -32 and vx < 32 and vy > -32 and vy < 32 then -- only draw if in range
        lg.draw(spritecoll, spritequads[orb_sprite], vx, vy + i)
      end
    end

    if i ~= 0 then
      -- only collide the orb with the ship, not with itself
      -- they are guaranteed to be in the same multiple of 4096
      vx = math.floor(ship.x-15.5) - topleftx
      vy = math.floor(ship.y-15.5) - toplefty
      lg.draw(spritecoll, spritequads[ship.angle + 1], vx, vy + i)
    end

    -- Collision with enemies
    local et
    for k, v in ipairs(enemies) do
      et = enemytypes[v.type]
      vx = math.floor(v.x/2)*2 - topleftx - (topleftx < et.width*32 and v.x >= 4096-et.width*32 and 4096 or 0)
      vy = math.floor(v.y/2)*2 - toplefty
      if vx > -(et.width*32) and vx < 32 and vy > -(et.height*32) and vy < 32 then
        -- visible
        lg.draw(spriteset, spritequads[v.f], vx, vy + i)
      end
    end

    -- Draw ship/orb in multiplicative mode
    lg.setBlendMode("multiplicative")
    lg.draw(spritecoll, spritequads[i == 0 and ship.angle+1 or orb_sprite], 0, i)
    lg.setBlendMode("alpha") -- return blend mode to normal
  end

  lg.setScissor()
  lg.setCanvas()
  return collision_canvas:getImageData():getString() ~= no_collision
end


function game.update(dt)
  garbagetimer = garbagetimer + dt
  if garbagetimer >= 5 then
    -- playing with Canvas:getImage tends to generate garbage that isn't
    -- collected, so we help Lua a bit here
    collectgarbage()
    garbagetimer = 0
  end

  lti.sleep(0.02) -- spare some CPU
  if dt > 0.05 then dt = 0.05 end -- smoothen the movement

  -- read keys
  local was_shooting = shooting
  shooting = lk.isDown(keys.fire)
  local pickup = lk.isDown(keys.pickup)

  if dt ~= 0 and collided() then
    -- Player died
    ship.x = respawn[counters.respawn].x
    ship.y = respawn[counters.respawn].y
    ship.oldx = ship.x
    ship.oldy = ship.y
    ship.angle = 0
    was_turning = false
    if orb then
      orb.x = ship.x
      orb.y = ship.y + tractor_length
      orb.oldx = orb.x
      orb.oldy = orb.y
    end
    if counters.shields == 0 then
      main.activate(gameover)
    end
    counters.shields = counters.shields - 1
  end

  -- Check if an orb was picked up
  tractor = pickup and tractor -- set to false if not picking up

  if tractor then
    if (orbs[tractor].x-ship.x)^2 + (orbs[tractor].y-ship.y)^2 >= tractor_length2 then
      -- Picked up orb
      orb = orbs[tractor]

      -- Remove the orb. The orbs table is unsorted, so for performance, to
      -- avoid scrolling (if removing a middle element) or creating a hole
      -- (if setting it to nil), we move the last element to this place.
      --table.remove(orbs, tractor) -- works, but this is presumably faster:
      orbs[tractor] = orbs[#orbs]
      table.remove(orbs)

      if #orbs % 4 == 1 then
        counters.shields = counters.shields + 1
      end

      game.state.orb = orb
      orb.oldx = orb.x
      orb.oldy = orb.y
      -- Sprites can't be deleted, so regenerate the batch.
      orbbatch:clear()

      -- Add decoys
      for k, v in ipairs(decoys) do
        orbbatch:add(spritequads[orb_sprite], v.x-16, v.y-16)
      end

      -- Add normal orbs
      for k, v in ipairs(orbs) do
        v.sprite = orbbatch:add(spritequads[orb_sprite], v.x-16, v.y-16)
      end

      -- This is an alternative to regenerating the batch, but still draws
      -- a 0x0 sprite.
      --orbbatch:set(orb.sprite, 0,0,0,0,0)

      orb.sprite = nil
      tractor = false
    end
  end

  -- Update player
  update_ship(dt)

  -- Clamp vertically
  if ship.y < 64 then ship.y = 64 end
  if ship.y > 2038 then ship.y = 2038 end

  -- Assign respawn zone
  for k, v in ipairs(respawn) do
    if ship.x%4096 >= v.topleftx and ship.x%4096 < v.topleftx + v.w
       and ship.y >= v.toplefty and ship.y < v.toplefty + v.h
    then
      counters.respawn = k
      break
    end
  end

  -- Target animation timer
  tgt_timer = tgt_timer + dt
  if tgt_timer / tgt_fps >= 1 then
    tgt_timer = tgt_timer % tgt_fps
    tgt_tile_current = (tgt_tile_current - tgt_tile_start + 1) % tgt_frames + tgt_tile_start
  end

  local tgt = targets[tgt_index]
  if orb and tgt and dt > 0 then
    -- Check if orb is in target
    local orbvelx = (orb.x - orb.oldx) / dt
    local orbvely = (orb.y - orb.oldy) / dt
    if orbvelx*orbvelx + orbvely*orbvely < tgt_vel_threshold2 then
      if     orb.x >= tgt.x*32 and orb.x < tgt.x*32+32
         and orb.y >= tgt.y*32 and orb.y < tgt.y*32+32
      then
        orb = nil
        game.state.orb = nil
        map[tgt.y*128+tgt.x+1] = tgt.tile
        tgt_index = tgt_index - 1
        -- Force refresh of map
        lastxtile = false
      end
    end
  end

  -- Agent timer
  if counters.agent then
    counters.agentinterval = counters.agentinterval + dt
    if counters.agentinterval / 0.625 >= 1 then
      counters.agentinterval = counters.agentinterval - 0.625
      counters.agenttime = counters.agenttime - 1
      if counters.agenttime <= 0 then
        counters.agenttime = nil
        counters.agent = nil
        counters.agentinterval = nil
      end
    end
  end

  -- Check if agent picked up.
  if pickup and (not counters.agenttime or counters.agenttime < 290) then
    local x, y = ship.x % 4096, ship.y + 64
    local t
    if y >= 0 and  y < 2048 then
      t = agents[map[math.floor(x/32) + math.floor(y/32)*128 + 1]]
      if t then
        counters.agent = t
        counters.agenttime = 300
        counters.agentinterval = 0
      end
    end
  end

  -- Shoot timer
  if counters.shoot_timer then
    counters.shoot_timer = counters.shoot_timer - dt
    if counters.shoot_timer <= 0 then
      counters.shoot_timer = nil
    end
  end

  -- Check if shooting
  if shooting and not was_shooting and counters.agent and not counters.shoot_timer then
    counters.shoot_timer = 4
    counters.shoot_x = ship.x
    counters.shoot_y = ship.y
    local x1, y1, x2, y2
    local radius2 = shoot_radius*shoot_radius
    -- did it hit any enemies?
    local k, v, l
    k = 1
    l = #enemies
    while k <= l do -- iterate manually to safely delete elements
      v = enemies[k]
      if v.type == counters.agent then
        -- the bounding box must be completely enclosed within the radius
        -- (not too realistic, but a collision analysis for this sounds
        -- overkill)
        x1 = v.x - ship.x
        y1 = v.y - ship.y
        x2 = x1 + enemytypes[v.type].width*32
        y2 = y1 + enemytypes[v.type].height*32
        x1 = x1*x1
        x2 = x2*x2
        y1 = y1*y1
        y2 = y2*y2
        if x1 + y1 < radius2
          and x1 + y2 < radius2
          and x2 + y1 < radius2
          and x2 + y2 < radius2
        then
          -- Killed enemy
          enemies[k] = enemies[l]
          l = l - 1
          table.remove(enemies)
        else
          k = k + 1
        end
      else
        k = k + 1
      end
    end
  end

  -- Check if tractor reaches orb
  if pickup and not orb and not tractor then
    -- Window for consideration
    local sx1, sy1, sx2, sy2 = ship.x-tractor_length, ship.y-tractor_length,
                               ship.x+tractor_length, ship.y+tractor_length

    for k, v in ipairs(orbs) do
      if v.x >= sx1 and v.x <= sx2 and v.y >= sy1 and v.y <= sy2 then
        -- *Might* be in range - do the more expensive Euclidean check
        if (v.x-ship.x)^2 + (v.y-ship.y)^2 <= tractor_length2 then
          -- In range - set tractor
          tractor = k
          break
        end
      end
    end

  end

  -- Update enemies' sprites and positions
  counters.clock = counters.clock + dt
  local t, tpos, et
  for k, v in ipairs(enemies) do
    -- Position
    tpos = (v.t + counters.clock) % v.period

    if tpos * 2 >= v.period then
      -- going back
      t = (v.period - tpos)*2 / v.period
    else
      t = tpos * 2 / v.period
    end

    -- Hack because our horizontal positions are broken
    if v.x0 == v.x1 then vx1 = v.x1 else vx1 = v.x1+2 end

    v.x = v.x0 * (1 - t) + vx1 * t
    v.y = v.y0 * (1 - t) + v.y1 * t

    -- Frame
    et = enemytypes[v.type]
--    t = (v.frame + counters.clock * v.fps) % et.nframes
    if vx1 ~= v.x0 then
      t = (tpos/(v.period*16)*(vx1-v.x0)*v.fps) % et.nframes
    else
      t = (tpos/(v.period*16)*(v.y1-v.y0)*v.fps) % et.nframes
    end
    if et.pingpong then
      if tpos * 2 >= v.period then -- going backwards
        t = (et.nframes-t) % et.nframes
      end
    end
    v.f = et.initsprite + math.floor(t)
  end

  -- Debug string
  -- game.DEBUG=tostring(collided()).. " " .. 1/dt

end

function game.tiles_draw(x, y)
  -- Clamp coordinates to acceptable values
  --if y < 0 then y = 0 end
  --if y > 2048 - game.vh then y = 2048 - game.vh end
  x = x % 4096

  local xtile = math.floor(x/32)
  local ytile = math.floor(y/32)
  local xtiles = math.floor(game.vw+62)/32 -- max visible tiles
  local ytiles = math.floor(game.vh+62)/32
  if ytile + ytiles > 64 then
    -- clamp vertically
    ytiles = 64 - ytile
  end

  if xtile ~= lastxtile or ytile ~= lastytile then
    -- update required
    lastxtile, lastytile = xtile, ytile

    tilebatch:clear()
    for yt = ytile, ytile + ytiles - 1 do
      for xt = xtile, xtile + xtiles - 1 do
        tilebatch:add(tilequads[map[yt*128 + xt%128 + 1]],
                      (xt-xtile)*32, (yt-ytile)*32)
      end
    end
  end
  lg.draw(tilebatch, -(x%32), -(y%32))

  lg.draw(orbbatch, -x, -y)
  -- Draw 1 screen to the left and/or 1 screen to the right if necessary
  if x < 32 then
    lg.draw(orbbatch, -x-4096, -y)
  end
  if x + game.vh >= 4064 then
    lg.draw(orbbatch, -x+4096, -y)
  end
end

function game.draw()
  local vpx = math.floor(ship.x+0.5-game.vw/2)
  local vpy = math.floor(ship.y+0.5-game.vh/2)
  if vpy < 0 then vpy = 0 end
  if vpy > 2048-game.vh then vpy = 2048-game.vh end
  lg.setScissor(game.vx, game.vy, game.vw, game.vh)
  game.tiles_draw(vpx, vpy)

  -- HACK: draw target in white first (we will colorize it later using multiplicative)
  local tgt = targets[tgt_index]
  if tgt then
    lg.draw(tileset, tilequads[75], tgt.x*32-vpx, tgt.y*32-vpy)
  end

  -- draw enemies
  local et
  for k, v in ipairs(enemies) do
    et = enemytypes[v.type]
    for x = v.x - 4096, vpx + game.vw, 4096 do
      if not (x >= vpx + game.vw
              or x+et.width*32 < vpx
              or v.y >= vpy + game.vh
              or v.y+et.height*32 < vpy)
      then
        -- visible
        lg.draw(spriteset, spritequads[v.f],
            (math.floor(x/2)*2 - vpx), (math.floor(v.y/2)*2 - vpy)
        )
      end
    end
  end

  -- draw orb and tractor line
  if orb or tractor then
    lg.setLineStyle("smooth")
    local torb = orb or orbs[tractor] -- tracted orb - either carried or still resting
    lg.line(torb.x-vpx, torb.y-vpy, ship.x-vpx, ship.y-vpy)
    lg.draw(spriteset, spritequads[orb_sprite], math.floor(torb.x-vpx-16), math.floor(torb.y-vpy-16))
  end
  -- draw ship
  lg.draw(spriteset, spritequads[ship.angle+1], math.floor(ship.x-vpx-15.5), math.floor(ship.y-vpy-15.5))

  -- HACK: draw target in multiplicative mode (colorizes other sprites)
  if tgt then
    lg.setBlendMode("multiplicative")
    lg.draw(tileset, tilequads[tgt_tile_current], tgt.x*32-vpx, tgt.y*32-vpy)
    lg.setBlendMode("alpha")
  end

  -- draw shooting explosion
  if counters.shoot_timer then
    lg.setColor(255, 255, 255, 255*(counters.shoot_timer/4))
    lg.circle("fill", (counters.shoot_x-vpx)%4096, counters.shoot_y-vpy, shoot_radius, 64)
    lg.setColor(255, 255, 255, 255)
  end

  -- FIXME: draw current agent
  if counters.agent then
    for k, v in pairs(agents) do
      if v == counters.agent then
        lg.draw(tileset, tilequads[k], 0, main.wh-32)
        lg.print(counters.agenttime, 35, main.wh-24)
      end
    end
  end
  -- FIXME: draw current shields
  lg.print((counters.shields < 10 and "0" or "") .. counters.shields, 96, main.wh-24)

  lg.setScissor()


  --[[ debug
  lg.print(game.DEBUG, 0, 0)
  -- draw collision canvas
  lg.setColor(100,100,100)
  lg.rectangle("fill", 100, 100, 32, 64)
  lg.setColor(255,255,255)
  lg.draw(collision_canvas, 100, 100)
  ]]
end

function game.keypressed(k, r)
  if r then return end
  if k == "pause" and (lk.isDown("lctrl") or lk.isDown("rctrl") or lk.isDown("ctrl")) then
    main.activate(menu)
  end
  if k == "f10" then game.savegame() end
  if k == "f3" then new_or_load_game(true) end
end

return game
